Reading and Controlling User Input in Flutter – Detailed Notes
User input is an essential part of interactive Flutter applications. Apps commonly need to accept information such as names, email addresses, passwords, search queries, messages, addresses, and other data from users.
Flutter provides widgets such as TextField and TextFormField for accepting text input. A TextEditingController can then be used to read, modify, clear, and listen to the text entered by the user. :contentReference[oaicite:0]{index=0}
1. What is User Input?
User input is information provided by a user through an application's interface. In Flutter, user input can come from text fields, buttons, checkboxes, switches, dropdowns, sliders, and other interactive widgets.
Common Examples of Text Input
- User name
- Email address
- Password
- Phone number
- Search keywords
- Address
- Comments
- Messages
- Product information
2. TextField for Taking User Input
TextField is one of the most commonly used Flutter widgets for accepting text input.
TextField(
decoration: InputDecoration(
labelText: 'Enter your name',
border: OutlineInputBorder(),
),
)
Flutter's TextField supports decoration such as labels, hints, icons, borders, and error-related UI. :contentReference[oaicite:1]{index=1}
3. TextFormField for Structured Forms
TextFormField is useful when text input is part of a Form and needs validation.
TextFormField(
decoration: InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
)
TextFormField integrates a text field with Flutter's Form and supports validation through a validator function. :contentReference[oaicite:2]{index=2}
4. Reading User Input with TextEditingController
The most common way to programmatically read text entered into a TextField is by using TextEditingController.
final nameController = TextEditingController();
TextField(
controller: nameController,
)
The current input can then be accessed using:
String name = nameController.text;
print(name);
Flutter's official documentation recommends creating a controller, assigning it to the text field, and reading the current value through the controller's text property. :contentReference[oaicite:3]{index=3}
5. Basic Complete Example
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const UserInputScreen(),
);
}
}
class UserInputScreen extends StatefulWidget {
const UserInputScreen({super.key});
@override
State createState() => _UserInputScreenState();
}
class _UserInputScreenState extends State {
final nameController = TextEditingController();
@override
void dispose() {
nameController.dispose();
super.dispose();
}
void readInput() {
final name = nameController.text;
print('User Name: $name');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('User Input'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: 'Enter your name',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: readInput,
child: const Text('Read Input'),
),
],
),
),
);
}
}
6. Understanding TextEditingController
TextEditingController controls the editable text associated with a text field. When the user changes the text, the controller's value changes and its listeners can be notified. You can also change the controller's text programmatically. :contentReference[oaicite:4]{index=4}
Important Controller Operations
| Operation |
Code |
Purpose |
| Read text |
controller.text |
Gets current user input |
| Set text |
controller.text = 'Hello' |
Changes input programmatically |
| Clear text |
controller.clear() |
Removes input |
| Listen for changes |
controller.addListener() |
Detects controller changes |
| Read editing value |
controller.value |
Gets complete editing state |
| Read selection |
controller.selection |
Gets current text selection |
| Release resources |
controller.dispose() |
Disposes the controller |
7. Reading Input Using onChanged
For simple cases, onChanged can be used to receive the latest text whenever the user changes the field.
TextField(
onChanged: (value) {
print('Current input: $value');
},
)
The callback runs whenever the text changes. Flutter's documentation identifies onChanged and TextEditingController as two common approaches for handling text changes. :contentReference[oaicite:5]{index=5}
Example: Live Character Count
TextField(
maxLength: 100,
onChanged: (value) {
print('Characters: ${value.length}');
},
)
8. onChanged vs TextEditingController
| Feature |
onChanged |
TextEditingController |
| Detect text changes |
Yes |
Yes |
| Read current text |
Callback value |
controller.text |
| Change text programmatically |
No |
Yes |
| Clear field programmatically |
No |
Yes |
| Access selection |
No |
Yes |
| Listen from multiple places |
Less convenient |
Yes |
| Requires disposal |
No |
Yes when controller is owned by the widget |
Use onChanged for simple change callbacks. Use a controller when your application needs broader control over the field's value. :contentReference[oaicite:6]{index=6}
9. Controlling User Input Programmatically
One of the major advantages of TextEditingController is that the application can modify the text field programmatically.
final nameController = TextEditingController();
nameController.text = 'Flutter Developer';
The connected text field will display the updated value.
Button Example
ElevatedButton(
onPressed: () {
nameController.text = 'Flutter Developer';
},
child: const Text('Set Default Text'),
)
10. Clearing User Input
Use the clear() method when the application needs to remove the current input.
nameController.clear();
Example
IconButton(
onPressed: () {
nameController.clear();
},
icon: const Icon(Icons.clear),
)
Clearing input is commonly used in search boxes, login forms, chat applications, and todo applications. Flutter's user-input tutorial also demonstrates clearing a controller after submitted input. :contentReference[oaicite:7]{index=7}
11. Checking Whether Input is Empty
if (nameController.text.isEmpty) {
print('Please enter your name');
} else {
print('Name: ${nameController.text}');
}
Using trim()
final name = nameController.text.trim();
if (name.isEmpty) {
print('Name is required');
}
Using trim() is useful when whitespace at the beginning or end of the input should not be considered meaningful.
12. Setting Initial User Input
A controller can be initialized with an initial value.
final nameController = TextEditingController(
text: 'John',
);
The connected field initially displays John. The controller constructor accepts an optional initial text value. :contentReference[oaicite:8]{index=8}
13. Listening to User Input Changes
Use addListener() when you want a function to be called whenever the controller's value changes.
final searchController = TextEditingController();
@override
void initState() {
super.initState();
searchController.addListener(() {
print(searchController.text);
});
}
The listener can be removed explicitly, although disposing the controller also removes its registered listeners.
@override
void dispose() {
searchController.dispose();
super.dispose();
}
Flutter's official documentation demonstrates registering listeners in initState() and disposing the controller in dispose(). :contentReference[oaicite:9]{index=9}
14. Reading User Input on Button Press
final emailController = TextEditingController();
ElevatedButton(
onPressed: () {
final email = emailController.text.trim();
print('Email: $email');
},
child: const Text('Submit'),
)
This pattern is commonly used when submitting login, registration, contact, or search forms.
15. Reading Multiple User Inputs
Applications frequently need to read multiple fields at once.
final nameController = TextEditingController();
final emailController = TextEditingController();
final phoneController = TextEditingController();
void submitForm() {
final name = nameController.text.trim();
final email = emailController.text.trim();
final phone = phoneController.text.trim();
print('Name: $name');
print('Email: $email');
print('Phone: $phone');
}
16. Complete User Information Example
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const UserFormScreen(),
);
}
}
class UserFormScreen extends StatefulWidget {
const UserFormScreen({super.key});
@override
State createState() => _UserFormScreenState();
}
class _UserFormScreenState extends State {
final nameController = TextEditingController();
final emailController = TextEditingController();
final phoneController = TextEditingController();
@override
void dispose() {
nameController.dispose();
emailController.dispose();
phoneController.dispose();
super.dispose();
}
void submit() {
final name = nameController.text.trim();
final email = emailController.text.trim();
final phone = phoneController.text.trim();
if (name.isEmpty || email.isEmpty || phone.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please fill all fields'),
),
);
return;
}
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('User Information'),
content: Text(
'Name: $name\nEmail: $email\nPhone: $phone',
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('OK'),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('User Information'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: 'Full Name',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: phoneController,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
labelText: 'Phone Number',
prefixIcon: Icon(Icons.phone),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: submit,
child: const Text('Submit'),
),
),
],
),
),
);
}
}
17. Reading Email Input
For email input, use an appropriate keyboard type and controller.
final emailController = TextEditingController();
TextField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email Address',
border: OutlineInputBorder(),
),
)
Read the value using:
final email = emailController.text.trim();
18. Reading Password Input
final passwordController = TextEditingController();
TextField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
)
Read the password when the user submits the form:
final password = passwordController.text;
In real applications, avoid unnecessarily printing or logging passwords.
19. Reading Search Input
A search field can use a controller to retrieve and control the search query.
final searchController = TextEditingController();
TextField(
controller: searchController,
decoration: InputDecoration(
hintText: 'Search products',
prefixIcon: const Icon(Icons.search),
suffixIcon: IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
searchController.clear();
},
),
border: const OutlineInputBorder(),
),
)
Search Function
void searchProducts() {
final query = searchController.text.trim();
if (query.isEmpty) {
return;
}
print('Searching for: $query');
}
20. Reading Multiline Input
Use maxLines when users need to enter longer content such as comments or messages.
final messageController = TextEditingController();
TextField(
controller: messageController,
maxLines: 5,
decoration: const InputDecoration(
labelText: 'Your Message',
border: OutlineInputBorder(),
),
)
Reading the Message
final message = messageController.text.trim();
print(message);
21. Controlling Maximum Input Length
Use maxLength to restrict the number of characters accepted by the field.
TextField(
controller: messageController,
maxLength: 200,
decoration: const InputDecoration(
labelText: 'Message',
),
)
This can be useful for usernames, short descriptions, comments, and messages.
22. Handling Submitted Input
The onSubmitted callback runs when the user submits the field using the keyboard action.
TextField(
controller: searchController,
onSubmitted: (value) {
print('Submitted: $value');
},
)
Clearing After Submission
TextField(
controller: searchController,
onSubmitted: (_) {
print(searchController.text);
searchController.clear();
},
)
Flutter's user-input tutorial demonstrates using onSubmitted together with a controller to capture and then clear submitted text. :contentReference[oaicite:10]{index=10}
23. Controlling Focus
Good input handling often includes focus management. A FocusNode can be connected to a text field and used to request focus programmatically.
final nameController = TextEditingController();
final nameFocusNode = FocusNode();
TextField(
controller: nameController,
focusNode: nameFocusNode,
)
Requesting Focus
nameFocusNode.requestFocus();
Disposing FocusNode
@override
void dispose() {
nameController.dispose();
nameFocusNode.dispose();
super.dispose();
}
Flutter recommends managing long-lived focus nodes with the widget's state lifecycle. :contentReference[oaicite:11]{index=11}
24. Moving Focus Between Fields
For forms with multiple fields, focus can be moved from one field to another.
final nameFocusNode = FocusNode();
final emailFocusNode = FocusNode();
TextField(
focusNode: nameFocusNode,
textInputAction: TextInputAction.next,
onSubmitted: (_) {
emailFocusNode.requestFocus();
},
)
TextField(
focusNode: emailFocusNode,
textInputAction: TextInputAction.done,
)
25. Validating User Input
Reading input and validating input are related but separate tasks. A form can validate user input before processing it.
final formKey = GlobalKey();
final emailController = TextEditingController();
Form(
key: formKey,
child: TextFormField(
controller: emailController,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Email is required';
}
return null;
},
),
)
Validate Before Submission
if (formKey.currentState!.validate()) {
final email = emailController.text.trim();
print('Valid email: $email');
}
Flutter's forms documentation uses Form, GlobalKey, and TextFormField.validator to validate user input. :contentReference[oaicite:12]{index=12}
26. Reading Input and Sending it to an API
User input can also be passed to a networking function when the user submits a form.
final titleController = TextEditingController();
void sendData() {
final title = titleController.text.trim();
if (title.isEmpty) {
return;
}
createData(title);
}
Future createData(String title) async {
print('Sending title: $title');
}
Flutter's networking cookbook demonstrates retrieving a value from a TextField through a controller and passing that value into a network request. :contentReference[oaicite:13]{index=13}
27. Practical Login Input Example
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State createState() => _LoginScreenState();
}
class _LoginScreenState extends State {
final emailController = TextEditingController();
final passwordController = TextEditingController();
@override
void dispose() {
emailController.dispose();
passwordController.dispose();
super.dispose();
}
void login() {
final email = emailController.text.trim();
final password = passwordController.text;
if (email.isEmpty || password.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please enter email and password'),
),
);
return;
}
print('Login requested for: $email');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Login'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: login,
child: const Text('Login'),
),
),
],
),
),
);
}
}
28. Practical Search Example with Live Input
class SearchScreen extends StatefulWidget {
const SearchScreen({super.key});
@override
State createState() => _SearchScreenState();
}
class _SearchScreenState extends State {
final searchController = TextEditingController();
final products = [
'Flutter Course',
'Dart Course',
'Android Course',
'iOS Course',
'Web Development Course',
];
List results = [];
@override
void initState() {
super.initState();
results = products;
searchController.addListener(filterProducts);
}
void filterProducts() {
final query = searchController.text.toLowerCase().trim();
setState(() {
results = products
.where(
(product) => product.toLowerCase().contains(query),
)
.toList();
});
}
@override
void dispose() {
searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Product Search'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: searchController,
decoration: InputDecoration(
hintText: 'Search products',
prefixIcon: const Icon(Icons.search),
suffixIcon: IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
searchController.clear();
},
),
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 16),
Expanded(
child: ListView.builder(
itemCount: results.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(results[index]),
);
},
),
),
],
),
),
);
}
}
This demonstrates how user input can be read continuously and used to update application data while the user types.
29. Resetting User Input
When a form contains multiple controllers, create a reset function.
void resetForm() {
nameController.clear();
emailController.clear();
phoneController.clear();
passwordController.clear();
}
Then connect it to a button:
OutlinedButton(
onPressed: resetForm,
child: const Text('Reset'),
)
30. Reading and Controlling Input with a Form
class RegistrationScreen extends StatefulWidget {
const RegistrationScreen({super.key});
@override
State createState() => _RegistrationScreenState();
}
class _RegistrationScreenState extends State {
final formKey = GlobalKey();
final nameController = TextEditingController();
final emailController = TextEditingController();
@override
void dispose() {
nameController.dispose();
emailController.dispose();
super.dispose();
}
void submit() {
if (!formKey.currentState!.validate()) {
return;
}
final name = nameController.text.trim();
final email = emailController.text.trim();
print('Name: $name');
print('Email: $email');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Registration'),
),
body: Form(
key: formKey,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextFormField(
controller: nameController,
decoration: const InputDecoration(
labelText: 'Name',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Name is required';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Email is required';
}
return null;
},
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: submit,
child: const Text('Register'),
),
),
],
),
),
),
);
}
}
31. Controller Lifecycle
A controller should normally follow the lifecycle of the stateful widget that owns it.
- Create: Declare the controller as a state field.
- Connect: Pass it to the
TextField or TextFormField.
- Read: Access the value using
controller.text.
- Control: Modify or clear the value when necessary.
- Listen: Use
addListener() when change notifications are required.
- Dispose: Call
dispose() when the widget is removed.
class ExampleState extends State {
final controller = TextEditingController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
);
}
}
Flutter's documentation specifically recommends disposing a controller when it is no longer needed. :contentReference[oaicite:14]{index=14}
32. Common Mistakes
Mistake 1: Forgetting dispose()
final controller = TextEditingController();
Better:
@override
void dispose() {
controller.dispose();
super.dispose();
}
Mistake 2: Creating the Controller Inside build()
@override
Widget build(BuildContext context) {
final controller = TextEditingController();
return TextField(
controller: controller,
);
}
A controller should generally have an appropriate lifecycle instead of being recreated every time build() runs.
Mistake 3: Not Checking Empty Input
final name = controller.text.trim();
if (name.isEmpty) {
return;
}
Mistake 4: Modifying Text Inside a Listener Without Care
controller.addListener(() {
controller.text = controller.text.toUpperCase();
});
Changing the controller inside its own listener can cause repeated notifications or loops. Flutter's API documentation specifically warns about this situation. :contentReference[oaicite:15]{index=15}
33. Best Practices for Reading and Controlling User Input
- Use
TextField for general text input.
- Use
TextFormField when the input belongs to a validated form.
- Use
TextEditingController when the application needs to read or control the input programmatically.
- Use
onChanged for simple text-change callbacks.
- Use
onSubmitted when input should be processed after keyboard submission.
- Use
trim() where leading and trailing whitespace should not matter.
- Use appropriate keyboard types such as email, phone, and number.
- Use
obscureText for password fields.
- Use validation before processing important form data.
- Dispose controllers when they are no longer needed.
- Dispose
FocusNode objects that your state owns.
- Avoid unnecessary controller recreation.
- Do not log sensitive information such as passwords.
34. Real-World Applications
| Application |
Input Example |
Typical Technique |
| Login |
Email and password |
TextEditingController + validation |
| Registration |
Name, email, phone |
Multiple controllers + Form |
| Search |
Product keyword |
Controller + listener/onChanged |
| Chat |
Message |
Controller + onSubmitted |
| Todo |
Task name |
Controller + clear() |
| Profile |
Name and address |
Initial values + controllers |
| API form |
Data fields |
Controller + network request |
35. Interview Questions
Q1. How can you read text entered in a TextField?
Use a TextEditingController and read its text property.
final value = controller.text;
Q2. How can you detect changes in user input?
Use either the onChanged callback or a TextEditingController listener.
Q3. How do you clear a TextField?
controller.clear();
Q4. How do you set text programmatically?
controller.text = 'Flutter';
Q5. Why should TextEditingController be disposed?
It is a disposable object and should be cleaned up when the owning widget no longer needs it.
Q6. What is the difference between TextField and TextFormField?
TextField provides general text input, while TextFormField integrates with Form and provides form-oriented features such as validation.
Q7. What is the purpose of FocusNode?
FocusNode allows the application to manage which input field currently has keyboard focus and to request or change focus programmatically.
36. Quick Revision
TextField accepts text input.
TextFormField is useful for form-based input and validation.
TextEditingController provides programmatic control over text.
- Read text with
controller.text.
- Set text with
controller.text = 'value'.
- Clear text with
controller.clear().
- Detect changes with
onChanged or addListener().
- Handle keyboard submission with
onSubmitted.
- Use
FocusNode to control input focus.
- Use
Form and validators when structured validation is required.
- Dispose controllers and focus nodes that your widget owns.
- Always validate and safely process important user input.
37. Official Flutter Resources
38. JustAcademy Flutter Training Resources
39. Key Takeaways
Reading and controlling user input is a fundamental Flutter skill. TextField and TextFormField provide the input interface, while TextEditingController gives the application programmatic access to the entered text. Developers can read values with text, update them programmatically, clear them, listen for changes, handle submission, manage focus, validate form data, and pass collected values to application logic or APIs.
Understanding these concepts makes it possible to build practical Flutter applications such as login screens, registration forms, search interfaces, chat screens, profile forms, todo apps, and API-powered data-entry screens.